package top.jacktgq.concurrent_utils.reentrant.readwrite.demo;
import	java.util.concurrent.locks.ReentrantReadWriteLock;

import top.jacktgq.utils.LogUtils;

import java.util.*;

/**
 * @Author CandyWall
 * @Date 2020/11/5--15:55
 * @Description 运用读写锁实现数据库数据的缓存
 */
public class TestGenericDaoWithReadWriteLock {
    public static void main(String[] args) {
        //demo1();
        demo2();
    }

    //查询三次数据库，按照相同的sql语句和参数去查询
    private static void demo1() {
        GenericDao dao = new GenericDao();
        LogUtils.log("TestGenericDaoWithReadWriteLock", "**************查询*************");
        String sql = "select * from emp where empno = ?";
        Integer empno = 9527;
        //查询三次数据库，按照相同的sql语句和参数去查询
        //这里可以用到缓存对相同的查询进行优化
        Emp emp = dao.queryOne(Emp.class, sql, empno);
        LogUtils.log("TestGenericDaoWithReadWriteLock", emp.toString());
        emp = dao.queryOne(Emp.class, sql, empno);
        LogUtils.log("TestGenericDaoWithReadWriteLock", emp.toString());
        emp = dao.queryOne(Emp.class, sql, empno);
        LogUtils.log("TestGenericDaoWithReadWriteLock", emp.toString());

        LogUtils.log("TestGenericDaoWithReadWriteLock", "**************更新*************");
        dao.update("update emp set sal = ? where empno = ?", 1000, empno);
        emp = dao.queryOne(Emp.class, sql, empno);
        LogUtils.log("TestGenericDaoWithReadWriteLock", emp.toString());
    }

    //对demo1中的GenericDao加了缓存的增强
    private static void demo2() {
        GenericDao dao = new CachedGenericDao1();
        LogUtils.log("TestGenericDaoWithReadWriteLock", "**************查询*************");
        String sql = "select * from emp where empno = ?";
        Integer empno = 9527;
        //查询三次数据库，按照相同的sql语句和参数去查询
        //这里可以用到缓存对相同的查询进行优化
        Emp emp = dao.queryOne(Emp.class, sql, empno);
        LogUtils.log("TestGenericDaoWithReadWriteLock", emp.toString());
        emp = dao.queryOne(Emp.class, sql, empno);
        LogUtils.log("TestGenericDaoWithReadWriteLock", emp.toString());
        emp = dao.queryOne(Emp.class, sql, empno);
        LogUtils.log("TestGenericDaoWithReadWriteLock", emp.toString());

        LogUtils.log("TestGenericDaoWithReadWriteLock", "**************更新*************");
        dao.update("update emp set sal = ? where empno = ?", 1000, empno);
        emp = dao.queryOne(Emp.class, sql, empno);
        LogUtils.log("TestGenericDaoWithReadWriteLock", emp.toString());
    }
}

/*
    版本1的进步之处：
        对原来负责进行数据库操做的Dao进行增强，让其拥有缓存的功能
        增加的写法用到了设计模式中的装饰器模式
    版本1暴露的问题：
        问题1、存储缓存数据的集合类cachedMap不是线程安全的，多线程下可能会问题
        问题2、更新数据的时候有两种策略：
            策略（1）先清空缓存，然后再去更新数据库
                问题：第一个线程将缓存清空完了，还没开始去更新数据库的时候，时间片用完了， 第二个线程拿到了时间片
                开始执行查询，这时候缓存里面没有数据，它就会去数据库里面查询，查到数据之后再将结果存一份到缓存中，
                然后它就执行完毕退出了；接着CPU时间片又分配给了第一个线程，它继续向下执行，去更新数据库，恰好更新
                的那条数据就是刚才第二个线程取的，然后它也执行完毕退出了。这就造成了数据库中的这条数据和缓存中的数
                据的不一致问题，并且往后其他线程再来执行对这条数据的查询操作都将是缓存中的旧数据（直到下一次更新缓
                存被清空），这种问题出现了很难被纠正。
            策略（2）先去更新数据库，再去清空缓存
                问题：第一个线程去修改了数据库中的某条数据，还没有去清空缓存的时候，时间片用完了，第二个线程拿到了
                时间片开始执行查询，并且恰好取到的是第一个线程更新的那条记录，缓存在cachedMap中的数据，执行完毕并退
                出。这就造成了缓存中的数据和数据库中的数据不一致的问题，不过这个只是暂时的不一致，当时间片又分配给了
                第一个线程，它继续向下执行，清空了缓存中的数据，执行完毕退出后，下一次对这条数据的查询就会直接去数
                据库查，然后再保存到缓存中，数据也就一致了。
            通过对两种更新策略的分析，第二种明显更优，而版本1用的是第一种，也是它的问题所在。
        问题3、当缓存中没有数据，正好又有大量线程要访问同一条记录，这时候它们都会去访问数据库，缓存没起到该有的作用。
*/
class CachedGenericDao1 extends GenericDao {
    private GenericDao genericDao = new GenericDao();
    //将数据库中以前查询的数据缓存在这个Map中
    private Map<SqlPair, Object> cacheMap = new HashMap<SqlPair, Object>();

    @Override
    public <T> List<T> queryList(Class<T> beanClass, String sql, Object... args) {
        return genericDao.queryList(beanClass, sql, args);
    }

    @Override
    public <T> T queryOne(Class<T> beanClass, String sql, Object... args) {
        //先从缓存中找，如果找到了就直接返回
        SqlPair key = new SqlPair(sql, args);
        T value = (T) cacheMap.get(key);
        if (value != null) {
            return value;
        }

        //缓存中没有再去查询数据库
        T t = genericDao.queryOne(beanClass, sql, args);
        //把查到的数据放一份到缓存中
        cacheMap.put(key, t);
        //返回从数据库查询到的结果
        return t;
    }

    @Override
    public int update(String sql, Object... args) {
        //先清空缓存
        cacheMap.clear();
        //再去更新数据库
        return genericDao.update(sql, args);
    }

    class SqlPair {
        private String sql;    //查询数据库用到的sql语句
        private Object[] args; //sql查询的条件

        public SqlPair(String sql, Object[] args) {
            this.sql = sql;
            this.args = args;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            SqlPair sqlPair = (SqlPair) o;
            return Objects.equals(sql, sqlPair.sql) &&
                    Arrays.equals(args, sqlPair.args);
        }

        @Override
        public int hashCode() {
            int result = Objects.hash(sql);
            result = 31 * result + Arrays.hashCode(args);
            return result;
        }
    }
}
/*
    版本2：解决了版本1中的问题
 */
class CachedGenericDao2 extends GenericDao {
    private GenericDao genericDao = new GenericDao();
    //将数据库中以前查询的数据缓存在这个Map中
    private /*volatile*/ Map<SqlPair, Object> cacheMap = new HashMap<SqlPair, Object>();
    //读写锁
    private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
    //读锁
    private ReentrantReadWriteLock.ReadLock readLock = rw.readLock();
    //写锁
    private ReentrantReadWriteLock.WriteLock writeLock = rw.writeLock();

    @Override
    public <T> List<T> queryList(Class<T> beanClass, String sql, Object... args) {
        return genericDao.queryList(beanClass, sql, args);
    }

    @Override
    public <T> T queryOne(Class<T> beanClass, String sql, Object... args) {
        SqlPair key = new SqlPair(sql, args);
        //给查找缓存操作添加读锁
        readLock.lock();
        try {
            //先从缓存中找，如果找到了就直接返回<这里是对cachedMap的读>
            T value = (T) cacheMap.get(key);
            if (value != null) {
                return value;
            }
        } finally {
            readLock.unlock();
        }

        // 获取写锁前必须先释放掉读锁，因为读锁不支持升级到写锁

        //给查询数据库和更新缓存添加写锁
        //如果多个线程查询的是同一条记录，并且都没有从缓存里面拿到数据，执行到这里等着去查询数据库
        //虽然加了锁保证每次只有一个线程去原子性地查询数据库并更新缓存，但是并不能阻止这些线程都去查询数据库并更新缓存
        //如果极端一点，有1000个线程都在这里排队等着拿到写锁进行下面的操作，显然是不合理的，
        //合理的情况是，其中一个拿到锁的线程应该查询数据库然后把数据存一份到缓存中，然后其他线程就去缓存里面读取即可
        //因此这里需要双重检测一下，到底缓存里面有没有数据
        writeLock.lock();
        try {
            T value = (T) cacheMap.get(key);
            if(value == null) {
                //缓存中没有再去查询数据库
                value = genericDao.queryOne(beanClass, sql, args);
                //把查到的数据放一份到缓存中<这里是对cachedMap的写>
                cacheMap.put(key, value);
            }
            //返回从缓存中获取到的数据或者数据库查询到的结果
            return value;
        } finally {
            writeLock.unlock();
        }
    }

    @Override
    public int update(String sql, Object... args) {
        //给修改操作添加写锁
        writeLock.lock();
        try {
            //先更新数据库
            int result = genericDao.update(sql, args);
            //再清空缓存
            cacheMap.clear();
            return result;
        } finally {
            writeLock.unlock();
        }
    }

    class SqlPair {
        private String sql;    //查询数据库用到的sql语句
        private Object[] args; //sql查询的条件

        public SqlPair(String sql, Object[] args) {
            this.sql = sql;
            this.args = args;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            SqlPair sqlPair = (SqlPair) o;
            return Objects.equals(sql, sqlPair.sql) &&
                    Arrays.equals(args, sqlPair.args);
        }

        @Override
        public int hashCode() {
            int result = Objects.hash(sql);
            result = 31 * result + Arrays.hashCode(args);
            return result;
        }
    }
}